﻿Partial Public Class Window
    Public Const UpArrow As Char = "↑"c
    Public Const DownArrow As Char = "↓"c
    Public Const UpDownArrow As Char = "↕"c

    Private Shared currentActiveWindow As Window
    Private Shared openWindows As New List(Of Window)
    Private Shared screenBuffer As New ConsoleScreenBuffer

    ''' <summary>
    ''' Gets the currently focused open window.
    ''' </summary>
    ''' <returns></returns>
    Public Shared ReadOnly Property ActiveWindow As Window
        Get
            Return currentActiveWindow
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets a value which indicates if the Run method should terminate when there are no open windows.
    ''' Default is True.
    ''' </summary>
    ''' <returns></returns>
    Public Shared Property StopIfNoWindowsOpen As Boolean = True

    Public Shared ReadOnly Property UserBuffer As New ConsoleScreenBuffer

    Protected Shared Sub ClearBackground(target As Window)
        If target.Visible Then
            Dim bnds As Rectangle = target.Bounds
            target.OffsetByParents(bnds)
            For y = bnds.Top To bnds.Bottom
                For x = bnds.Left To bnds.Right
                    screenBuffer.Item(x, y) = " "c
                    screenBuffer.ForegroundColor(x, y) = target.ForeColor
                    screenBuffer.BackgroundColor(x, y) = target.BackColor
                Next
            Next
        End If
    End Sub

    Protected Shared Sub ClearChildBackgrounds(children As ChildWindowCollection)
        For Each child In children
            If child.Visible Then
                Dim bnds As Rectangle = child.Bounds
                child.OffsetByParents(bnds)
                For y = bnds.Top To bnds.Bottom
                    For x = bnds.Left To bnds.Right
                        screenBuffer.Item(x, y) = " "c
                        screenBuffer.ForegroundColor(x, y) = child.ForeColor
                        screenBuffer.BackgroundColor(x, y) = child.BackColor
                    Next
                Next
                DrawText(child)
                ClearChildBackgrounds(child.Children)
            End If
        Next
    End Sub

    Protected Shared Sub DrawText(target As Window)
        If Not String.IsNullOrEmpty(target.Content.Text) Then
            'draw window's text
            Dim x As Integer = target.ClientBounds.Left
            Dim y As Integer = target.ClientBounds.Top
            target.OffsetByParents(x, y)
            Dim top As Integer = y
            Dim bottom As Integer = y + target.ClientBounds.Height - 1
            Dim lines = target.Content.Lines
            Dim dx, dy As Integer

            For i As Integer = 0 To target.ClientBounds.Height - 1
                dy = y
                Dim line As String
                If target.InvertTextDirection Then
                    dy = top + bottom - y
                    line = target.Content.GetLineText(lines(lines.Count - 1 - target.firstLineVisible - i))
                Else
                    line = target.Content.GetLineText(lines(target.firstLineVisible + i))
                End If
                If Not String.IsNullOrEmpty(line) Then
                    For dx = 0 To line.Length - 1
                        Dim c = line(dx)
                        If Not Char.IsControl(c) Then
                            screenBuffer(x + dx, dy) = c
                            screenBuffer.ForegroundColor(x + dx, dy) = target.GetForecolor(x + dx, y)
                            screenBuffer.BackgroundColor(x + dx, dy) = target.GetBackcolor(x + dx, y)
                        End If

                    Next
                End If
                y += 1
            Next
            'update scroll and cursor position info
            target.textCursorLeft = dx + x + 1
            target.textCursorTop = y - 1
            target.atBottom = (target.firstLineVisible + target.ClientBounds.Height >= lines.Count)
            If Not target.previousFirstVisible = target.firstLineVisible Then
                target.OnFirstVisibleLineChanged(EventArgs.Empty)
                target.previousFirstVisible = target.firstLineVisible
            End If
            'update scroll bar characters
            If target.Content.LineCount > target.ClientBounds.Height Then
                If target.ClientBounds.Height = 1 Then
                    If Not target.atBottom OrElse target.firstLineVisible > 0 Then
                        Dim wx As Integer = target.Bounds.Right - 1
                        Dim wy As Integer = target.Bounds.Top + 1
                        If Not target.FrameVisible Then wy -= 1
                        If Not target.FrameVisible Then wx += 1
                        target.OffsetByParents(wx, wy)
                        screenBuffer.Item(wx, wy) = UpDownArrow
                    End If
                Else
                    Dim ddx As Integer = target.Bounds.Right - 1
                    Dim ddy As Integer = target.Bounds.Top + 1
                    target.OffsetByParents(ddx, ddy)
                    If Not target.FrameVisible Then ddy -= 1
                    If Not target.FrameVisible Then ddx += 1
                    For yy = ddy To ddy + target.ClientBounds.Height - 1
                        screenBuffer.Item(ddx, yy) = "│"c
                    Next

                    If target.InvertTextDirection Then
                        If Not target.atBottom Then
                            Dim wx As Integer = target.Bounds.Right - 1
                            Dim wy As Integer = target.Bounds.Top + 1
                            target.OffsetByParents(wx, wy)
                            If Not target.FrameVisible Then wy -= 1
                            If Not target.FrameVisible Then wx += 1
                            screenBuffer.Item(wx, wy) = UpArrow
                        End If
                        If target.firstLineVisible > 0 Then
                            Dim wx As Integer = target.Bounds.Right - 1
                            Dim wy As Integer = target.Bounds.Bottom - 1
                            target.OffsetByParents(wx, wy)
                            If Not target.FrameVisible Then wy += 1
                            If Not target.FrameVisible Then wx += 1
                            screenBuffer.Item(wx, wy) = DownArrow
                        End If
                    Else
                        If Not target.atBottom Then
                            Dim wx As Integer = target.Bounds.Right - 1
                            Dim wy As Integer = target.Bounds.Bottom - 1
                            target.OffsetByParents(wx, wy)
                            If Not target.FrameVisible Then wy += 1
                            If Not target.FrameVisible Then wx += 1
                            screenBuffer.Item(wx, wy) = DownArrow
                        End If
                        If target.firstLineVisible > 0 Then
                            Dim wx As Integer = target.Bounds.Right - 1
                            Dim wy As Integer = target.Bounds.Top + 1
                            target.OffsetByParents(wx, wy)
                            If Not target.FrameVisible Then wy -= 1
                            If Not target.FrameVisible Then wx += 1
                            screenBuffer.Item(wx, wy) = UpArrow
                        End If
                    End If

                End If
            End If
            target.OnPostDraw()
        End If
    End Sub

    ''' <summary>
    ''' Draws all open windows to the screen buffer, then redraws changes in the screen buffer to the console window.
    ''' </summary>
    ''' <returns></returns>
    Public Shared Function DrawOpenWindows() As Boolean
        screenBuffer.BeginInvalidate()
        Dim didDraw As Boolean = False
        For Each w In From ow In openWindows Where ow.Parent Is Nothing
            If w.Visible Then
                ClearBackground(w)
                DrawText(w)
                ClearChildBackgrounds(w.Children)
                DrawWindowFrames(w)
                didDraw = True
            ElseIf w.needsErased
                EraseWindow(w)
                Dim eraseChildren As New Action(Of ChildWindowCollection)(Sub(children)
                                                                              For Each child In children
                                                                                  EraseWindow(child)
                                                                                  eraseChildren(child.Children)
                                                                              Next
                                                                          End Sub)
                eraseChildren(w.Children)
                w.needsErased = False
            End If
        Next
        UserBuffer.CopyTo(screenBuffer)
        screenBuffer.EndInvalidate()
        screenBuffer.Refresh()
        Return didDraw
    End Function

    Protected Shared Sub DrawWindowFrames(target As Window)
        Dim frameGroups As Dictionary(Of Window, List(Of Rectangle)) = GatherFrameBounds(target)
        For Each w In frameGroups.Keys
            FrameRenderer.DrawFrame(w.FrameColor, w.BackColor, screenBuffer, frameGroups(w).ToArray)
        Next

    End Sub

    Protected Shared Sub EraseWindow(target As Window)
        Dim bnds As Rectangle = target.Bounds
        target.OffsetByParents(bnds)
        For y = bnds.Top To bnds.Bottom
            For x = bnds.Left To bnds.Right
                screenBuffer.Item(x, y) = " "c
                screenBuffer.ForegroundColor(x, y) = Console.ForegroundColor
                screenBuffer.BackgroundColor(x, y) = Console.BackgroundColor
            Next
        Next
    End Sub

    Protected Shared Function GatherFrameBounds(target As Window) As Dictionary(Of Window, List(Of Rectangle))
        Dim result As New Dictionary(Of Window, List(Of Rectangle))
        If target.Visible Then
            Dim localResult As New List(Of Rectangle)
            Dim localBounds As Rectangle = target.Bounds
            target.OffsetByParents(localBounds)
            If target.FrameVisible Then localResult.Add(localBounds)
            localResult.AddRange(GetMergedChildFrames(target.Children))
            result.Add(target, localResult)
            GetStandaloneChildFrames(target.Children, result)
        End If
        Return result
    End Function

    Protected Shared Function GetMergedChildFrames(children As ChildWindowCollection) As List(Of Rectangle)
        Dim result As New List(Of Rectangle)
        For Each child In children
            If child.Visible AndAlso child.MergeFrameWithParent AndAlso child.FrameVisible Then
                Dim childBounds = child.Bounds
                child.OffsetByParents(childBounds)
                result.Add(childBounds)
                result.AddRange(GetMergedChildFrames(child.Children))
            End If
        Next
        Return result
    End Function

    Protected Shared Sub GetStandaloneChildFrames(children As ChildWindowCollection, result As Dictionary(Of Window, List(Of Rectangle)))
        For Each child In children
            If child.Visible Then
                If Not child.MergeFrameWithParent AndAlso child.FrameVisible Then
                    For Each kvp In GatherFrameBounds(child)
                        result(kvp.Key) = kvp.Value
                    Next
                End If
                GetStandaloneChildFrames(child.Children, result)
            End If
        Next
    End Sub

    Protected Shared Sub PerformOpenWindowUpdates()
        For Each w In openWindows
            w.OnUpdate(EventArgs.Empty)
        Next
    End Sub

    ''' <summary>
    ''' Begins an execution loop which processes open windows.
    ''' </summary>
    ''' <param name="throttleMS">An optional delay (in milliseconds) used to throttle CPU usage. Default is 1ms.</param>
    Public Shared Sub Run(Optional throttleMS As Integer = 1)
        NativeMethods.SetConsoleMode(NativeMethods.GetStdHandle(-11), 1)
        Console.BufferWidth = Console.WindowWidth
        Console.BufferHeight = Console.WindowHeight
        screenBuffer.Resize(New Rectangle(0, 0, Console.BufferWidth, Console.BufferHeight))
        UserBuffer.Resize(New Rectangle(0, 0, Console.BufferWidth, Console.BufferHeight))
        UserBuffer.ClearBuffer()
        Dim bufferWidthDelta As Integer = Console.BufferWidth
        Dim bufferHeightDelta As Integer = Console.BufferHeight
        Do
            If openWindows.Count = 0 Then Exit Do
            If Not Console.BufferWidth = bufferWidthDelta OrElse Not Console.BufferHeight = bufferHeightDelta Then
                Console.Clear()
                Console.BufferWidth = Console.WindowWidth
                Console.BufferHeight = Console.WindowHeight
                screenBuffer.Resize(New Rectangle(0, 0, Console.BufferWidth, Console.BufferHeight))
                screenBuffer.ClearBuffer()
                UserBuffer.Resize(New Rectangle(0, 0, Console.BufferWidth, Console.BufferHeight))
                UserBuffer.ClearBuffer()
                bufferWidthDelta = Console.BufferWidth
                bufferHeightDelta = Console.BufferHeight
            End If
            If Not UpdateWindows() Then
                If StopIfNoWindowsOpen Then Exit Do
            End If
            Threading.Thread.Sleep(throttleMS)
        Loop
    End Sub

    Protected Shared Sub UpdateActiveWindow()
        Dim curActive = (From w In openWindows Where w.Visible AndAlso w.CanFocus).LastOrDefault
        If Not curActive Is currentActiveWindow Then
            currentActiveWindow?.OnLostFocus(EventArgs.Empty)
            currentActiveWindow = curActive
            curActive?.OnGotFocus(EventArgs.Empty)
        End If
    End Sub

    ''' <summary>
    ''' Updates and redraws all open windows. This method is called internally by Run or can be called
    ''' manually from within a custom program execution loop when Window.Run is not used.
    ''' </summary>
    ''' <returns></returns>
    Public Shared Function UpdateWindows() As Boolean
        UpdateActiveWindow()
        If Console.KeyAvailable Then
            Dim key = Console.ReadKey(True)
            ActiveWindow?.ProcessKey(key)
        End If
        PerformOpenWindowUpdates()
        Return DrawOpenWindows()
    End Function
End Class
